<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>11装饰者模式</title>
  </head>
  <body>
    <p>
      在程序开发中，许多时候都并不希望某个类天
      生就非常庞大，一次性包含许多职责。那么我们就可以使用装饰者模式。装饰者模式可以动态地
      给某个对象添加一些额外的职责，而不会影响从这个类中派生的其他对象。
    </p>

    <p>
      这种给对象动态地增加职责的方式称为装饰者（decorator）模式。装饰者模式能够在不改
      变对象自身的基础上，在程序运行期间给对象 动态地添加职责。
    </p>

    <p>
      在 JavaScript 中，几乎一切都是对象，其中函数又被称为一等对象。在平时的开发工作中，
      也许大部分时间都在和函数打交道。在 JavaScript 中可以很方便地给某个对象扩展属性和方法，
      但却很难在不改动某个函数源代码的情况下，给该函数添加一些额外的功能。在代码的运行期间，
      我们很难切入某个函数的执行环境。
    </p>
    用户名：<input id="username" type="text" /> 密码：
    <input id="password" type="password" />
    <input id="submitBtn" type="button" value="提交" />
    <button tag="login" id="button">点击打开登录浮层</button>
    <script>
      // 动态装饰函数的方式
      // ===============================
      // JavaScript 的装饰者 (通过保存原引用的方式就可以改写某个函数)
      let plane = {
        fire: function () {
          console.log('发射普通子弹');
        },
      };

      let missileDecorator = function () {
        console.log('发射导弹');
      };
      let atomDecorator = function () {
        console.log('发射原子弹');
      };

      let fire1 = plane.fire;
      plane.fire = function () {
        fire1();
        missileDecorator();
      };
      let fire2 = plane.fire;
      plane.fire = function () {
        fire2();
        atomDecorator();
      };
      plane.fire();

      // AOP 装饰函数
      Function.prototype.__before = function (beforefn) {
        const __self = this; // 保存原函数的引用
        return function () {
          // 返回包含了原函数和新函数的"代理"函数
          beforefn.apply(this, arguments); // 执行新函数，且保证 this 不被劫持，新函数接受的参数
          // 也会被原封不动地传入原函数，新函数在原函数之前执行
          return __self.apply(this, arguments); // 执行原函数并返回原函数的执行结果，
          // 并且保证 this 不被劫持
        };
      };

      Function.prototype.__after = function (afterfn) {
        const __self = this;
        return function () {
          const ret = __self.apply(this, arguments);
          afterfn.apply(this, arguments);
          return ret;
        };
      };

      document.getElementById = document.getElementById.__before(function () {
        alert(1);
      });

      //  值得提到的是，上面的 AOP 实现是在 Function.prototype 上添加 before 和 after 方法，但许
      // 多人不喜欢这种污染原型的方式，那么我们可以做一些变通，把原函数和新函数都作为参数传入
      // before 或者 after 方法：
      const $$before = function (fn, beforefn) {
        return async function () {
          beforefn.apply(this, arguments);
          return fn.apply(this, arguments);
        };
      };

      const $$after = function (fn, afterfn) {
        return function () {
          let ret = fn.apply(this, arguments);
          afterfn.apply(this, arguments);
          return ret;
        };
      };

      let a = $$before(
        function () {
          alert(3);
        },
        function () {
          alert(2);
        }
      );

      a = $$before(a, function () {
        alert(1);
      });
      a();

      // ===============================
      //  比如页面中有一个登录 button，点击这个 button 会弹出登录浮层，与此同时要进行数据上报，
      // 来统计有多少用户点击了这个登录 button：
      var showLogin = function () {
        console.log('打开登录浮层');
      };
      var log = function () {
        console.log('上报标签为: ' + this.getAttribute('tag'));
      };
      showLogin = showLogin.__after(log); // 打开登录浮层之后上报数据
      document.getElementById('button').onclick = showLogin;

      // ===============================
      // 下面的例子展示了如何通过 Function.prototype.before 方法给函数 func 的参数 param 动态地
      // 添加属性 b：
      let func = function (param) {
        console.log(param); // 输出： {a: "a", b: "b"}
      };
      func = func.__before(function (param) {
        param.b = 'b';
      });
      func({ a: 'a' });

      // 然后把 Token 参数通过 Function.prototyte.before 装饰到 ajax 函数的参数 param 对象中：
      const getToken = function () {
        return 'Token';
      };
      ajax = ajax.__before(function (type, url, param) {
        param.Token = getToken();
      });
      ajax('get', 'http:// xxx.com/userinfo', { name: 'sven' });
      // 从 ajax 函数打印的 log 可以看到，Token 参数已经被附加到了 ajax 请求的参数中：
      // {name: "sven", Token: "Token"}

      // =============================== 插件式的表单验证
      let username = document.getElementById('username'),
        password = document.getElementById('password'),
        submitBtn = document.getElementById('submitBtn');

      Function.prototype.__before__ = function (beforefn) {
        const __self = this;
        return function () {
          if (beforefn.apply(this, arguments) === false) {
            // beforefn 返回 false 的情况直接 return，不再执行后面的原函数
            return;
          }
          return __self.apply(this, arguments);
        };
      };

      // const $_before = function (fn, beforefn) {
      //   return function () {
      //     const beforeRet = beforefn.apply(this, arguments);
      //     if (!beforeRet) return;
      //     return fn.apply(this, arguments);
      //   };
      // };

      const validata = function () {
        if (username.value === '') {
          alert('用户名不能为空');
          return false;
        }
        if (password.value === '') {
          alert('密码不能为空');
          return false;
        }
      };

      let formSubmit = function () {
        const param = {
          username: username.value,
          password: password.value,
        };
        ajax('http:// xxx.com/login', param);
      };

      // formSubmit = $_before(formSubmit, validata);

      formSubmit = formSubmit.__before__(validata);

      submitBtn.onclick = function () {
        formSubmit();
      };
      // 执行 execute
      // ==========总结==========
      //   代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是，当直
      //   接访问本体不方便或者不符合需要时，为这个本体提供一个替代者。本体定义了关键功能，而代
      //   理提供或拒绝对它的访问，或者在访问本体之前做一些额外的事情。装饰者模式的作用就是为对
      //   象动态加入行为。换句话说，代理模式强调一种关系（Proxy 与它的实体之间的关系），这种关系
      //   可以静态的表达，也就是说，这种关系在一开始就可以被确定。而装饰者模式用于一开始不能确
      //   定对象的全部功能时。代理模式通常只有一层代理本体的引用，而装饰者模式经常会形成一条
      //   长长的装饰链。
      //   在虚拟代理实现图片预加载的例子中，本体负责设置 img 节点的 src，代理则提供了预加载
      //   的功能，这看起来也是“加入行为”的一种方式，但这种加入行为的方式和装饰者模式的偏重点
      //   是不一样的。装饰者模式是实实在在的为对象增加新的职责和行为，而代理做的事情还是跟本体
      //   一样，最终都是设置 src。但代理可以加入一些“聪明”的功能，比如在图片真正加载好之前，
      //   先使用一张占位的 loading 图片反馈给客户。
      //
    </script>
  </body>
</html>
